/*
 * Copyright (C) 2008 Alessandro Sivieri <alessandro.sivieri@chimera-bellerofonte.eu>.
 *
 *  This file is part of Daex.
 *
 *  Daex is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Foobar is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 */
package eu.chimera_bellerofonte.tesi.encoding;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.logging.Level;

import eu.chimera_bellerofonte.tesi.DaexLogger;
import eu.chimera_bellerofonte.tesi.DaexProperties;
import eu.chimera_bellerofonte.tesi.datatypes.Song;


/**
 * <p>Classe di gestione dell'encoding di default.</p>
 * <p>Si basa sulla gestione parallela di due code, la prima
 * dedicata al ripping delle tracce dal dvd a wave, la
 * seconda codifica in parallelo tali tracce a mano a mano
 * che vengono aggiunte.
 * <br />
 * Tale gestione, effettuata tramite eventi sulle proprieta',
 * permette il passaggio degli oggetti conclusi dalla prima
 * verso la seconda, per terminare le transazioni.</p>
 * 
 * @author Sivieri Alessandro
 * @author Vitucci Nicola
 * @version 02/giu/07
 */
public class DefaultEncoderExecutor implements EncoderExecutor, PropertyChangeListener
{
	private EncodingList list=null;
	private boolean encoding=false;
	private boolean breaked=false;
	private boolean fallen=false;
	private PropertyChangeSupport support=null;
	private QueueEncoderExecutor firstExecutor=null;
	private QueueEncoderExecutor secondExecutor=null;
	
	/**
	 * Costruttore; setta gli attributi di classe con i
	 * valori specificati.
	 * 
	 * @param list la lista con i dettagli di encoding
	 * @param bar la progress bar da gestire
	 * @param model il modello creato
	 * @param console la console dove inserire l'output
	 * @throws IOException eccezione lanciata in caso di problemi nella creazione di files temporanei
	 */
	public DefaultEncoderExecutor(EncodingList list) throws IOException
	{
		this.list = list;
		this.support=new PropertyChangeSupport(this);
		for(Song s: list.getSongList())
		{
			if(s.getTempFile()==null)
			{
				setTempFile(File.createTempFile("daex", ".wav", new File(list.getTempDirectory())), s.getTitle());
			}
		}
		try
		{
			firstExecutor=new QueueEncoderExecutor(EncodingOps.OPRIP);
			secondExecutor=new QueueEncoderExecutor(EncodingOps.OPENC);
		}
		catch (ExecutionException e)
		{
			// ignorato, le due operazioni passate sono corrette
		}
		Runtime.getRuntime().addShutdownHook(new Thread()
		{
			public void run()
			{
				File[] files=new File(DaexProperties.getProperty("directory.temp")).listFiles();
				if(files.length>0)
				{
					DaexLogger.getDaexLogger().info("Trovati "+files.length+" files temporanei, li elimino...");
					for(File f: files)
						f.delete();
				}
				File outdir=new File(DefaultEncoderExecutor.this.list.getOutDirectory());
				if(outdir.list().length==0)
				{
					outdir.delete();
				}
				File outdir2=outdir.getParentFile();
				if(outdir2.list().length==0)
				{
					outdir2.delete();
				}
			}
		});
	}
	
	private void setTempFile(File f, String title)
	{
		for(Song s: list.getSongList())
		{
			if(s.getTitle().equals(title))
			{
				s.setTempFile(f);
			}
		}
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#addPropertyChangeListener(String, java.beans.PropertyChangeListener)
	 */
	public void addPropertyChangeListener(String property, PropertyChangeListener listener)
	{
		support.addPropertyChangeListener(property, listener);
	}

	/**
	 * @throws ProgramNotFoundException 
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#encode()
	 */
	public void encode() throws ProgramNotFoundException
	{
		encoding=true;
		support.firePropertyChange(EXECPROPERTY, !encoding, encoding);
		firstExecutor.addPropertyChangeListener(this);
		secondExecutor.addPropertyChangeListener(this);
		File outdir=new File(list.getOutDirectory());
		if(!outdir.exists())
		{
			outdir.mkdirs();
		}
		firstExecutor.encode();
		secondExecutor.encode();
		firstExecutor.setBaseProgram(Program.getInstance(Program.wav));
		firstExecutor.setLast(true);
		firstExecutor.addSongList(list.getSongList());
		synchronized(firstExecutor)
		{
			firstExecutor.notifyAll();
		}
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#getPropertyChangeListeners(String)
	 */
	public PropertyChangeListener[] getPropertyChangeListeners(String property)
	{
		return support.getPropertyChangeListeners(property);
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#interrupt()
	 */
	public void interrupt()
	{
		firstExecutor.interrupt();
		secondExecutor.interrupt();
		encoding=false;
		synchronized(this)
		{
			notifyAll();
		}
		support.firePropertyChange(EXECPROPERTY, !encoding, encoding);
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#isEncoding()
	 */
	public boolean isEncoding()
	{
		return encoding;
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#removePropertyChangeListener(String, java.beans.PropertyChangeListener)
	 */
	public void removePropertyChangeListener(String partial, PropertyChangeListener listener)
	{
		support.removePropertyChangeListener(partial, listener);
	}
	
	/**
	 * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
	 */
	public void propertyChange(PropertyChangeEvent evt)
	{
		if(evt.getSource().equals( firstExecutor ) && (EXECPARTIAL.equals( evt.getPropertyName() ) || EXECLASTPARTIAL.equals( evt.getPropertyName() )))
		{
			secondExecutor.addSong(firstExecutor.getSong());
			if(EXECLASTPARTIAL.equals( evt.getPropertyName() ))
			{
				secondExecutor.setLast(true);
			}
			synchronized(secondExecutor)
			{
				secondExecutor.notifyAll();
			}
		}
		else if(evt.getSource().equals( secondExecutor ) && EXECPROPERTY.equals( evt.getPropertyName() ) && evt.getNewValue().equals(new Boolean(false)))
		{
			if(!breaked)
			{
				support.firePropertyChange(PROGRESSION, -1, 100);
				support.firePropertyChange(EXECOUTPUT, null, "\nConcluso.");
			}
			encoding=false;
			synchronized(this)
			{
				notifyAll();
			}
			support.firePropertyChange(EXECPROPERTY, !encoding, encoding);
		}
		else if(EXECPROPERTY.equals( evt.getPropertyName() ) && evt.getNewValue().equals(new Boolean(false)) && fallen)
		{
			interrupt();
		}
		// else non gestito
	}

	/**
	 * <p>Esecutore a coda del sistema, accetta e resta in attesa
	 * sulla lista.</p>
	 * <p>Classe per l'esecuzione dell'encoding basato su SwingWorker;
	 * questa metodologia permette di poter visualizzare il progresso
	 * delle operazioni, di default tramite una progress bar, una lista
	 * di avanzamento ed un documento cui si riferiscono gli output dei
	 * comandi eseguiti.</p>
	 * <p>Vengono richiamati metodi che assicurano un refresh immediato
	 * delle liste o dei documenti, in modo da mantenere la parte visiva
	 * di progresso il piu' in sincrono possibile con cio' che viene
	 * effettivamente eseguito.</p>
	 * 
	 * @see eu.chimera_bellerofonte.tesi.encoding.SwingWorker
	 * @see javax.swing.JProgressBar
	 * @see javax.swing.ListModel
	 * @see javax.swing.text.Document
	 * @author Sivieri Alessandro
	 * @author Vitucci Nicola
	 * @version 01/giu/07
	 */
	protected class QueueEncoderExecutor extends SwingWorker implements
			EncoderExecutor
	{
		private final EncodingOps op;
		private PropertyChangeSupport support=null;
		private boolean encoding=false;
		private boolean last=false;
		private Queue<Song> queue=null;
		private Process p=null;
		private Program baseProgram=null;
		private Song song=null;
		private int j=0;

		/**
		 * Costruttore; setta gli attributi di classe con i
		 * valori specificati.
		 * 
		 * @param op l'operazione di default dell'encoder
		 * @throws ExecutionException eccezione lanciata in caso di problemi
		 */
		public QueueEncoderExecutor(EncodingOps op) throws ExecutionException
		{
			if(!EncodingOps.OPRIP.equals( op ) && !EncodingOps.OPENC.equals( op ))
			{
				throw new ExecutionException("Operazione errata in costruzione");
			}
			this.op=op;
			this.support=new PropertyChangeSupport(this);
			queue=new LinkedList<Song>();
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.SwingWorker#construct()
		 */
		@Override
		public Object construct()
		{
			ArrayList<String> commands=null;
			int total=list.getSongList().length;
			int increment=100/total;
			int actual=0;
			
			// aiuto nel debug
			if(op.equals( EncodingOps.OPRIP ))
			{
				Thread.currentThread().setName("Ripping thread");
			}
			else
			{
				Thread.currentThread().setName("Encoding thread");
			}
			try
			{
				support.firePropertyChange(PROGRESSION, -1, 0);
				// condizioni in modo da proseguire fino a che non ci viene detto
				// di smettere ed in ogni caso fino a che ci sono elementi da
				// codificare
				while(!last || queue.size()>0)
				{
					DaexLogger.getDaexLogger().fine(Thread.currentThread().getName()+": entering loop with last="+last+", queue size: "+queue.size());
					synchronized(this)
					{
						while(queue.size()==0)
						{
							wait();
						}
					}
					song=queue.poll();
					// viene eseguito dal primo thread di questo tipo
					if(op.equals( EncodingOps.OPRIP ))
					{
						// costruisco i comandi
						baseProgram.setOutput(song.getTempFile());
						// posso usare DvdNative come cast poiche' questa implementazione e' legata a DvdNative
						commands=baseProgram.getExecCommand(song, list.getDvdTitle(), song.getEncoder().getQuality(), list.getDvd().getDevice(), list.getOutDirectory()); // input usato come dvd device
						if(!song.getEncoder().needIntermediateWave())
						{
							song.getEncoder().getEncoderProgram().setOutputString(baseProgram.getOutputString());
						}
						// inizializzo le visualizzazioni (se presenti)
						support.fireIndexedPropertyChange(PARTIALOP, j, null, EncodingOps.OPRIP);
						support.firePropertyChange(EXECOUTPUT, null, "\nAttendo l'output...");
						// esecuzione del ripping
						exec(convertArray(commands));
					}
					else // viene eseguito dal secondo thread
					{
						if(song.getEncoder().needIntermediateWave())
						{
							commands=song.getEncoder().getEncoderProgram().getExecCommand(song, list.getDvdTitle(), song.getEncoder().getQuality(), song.getTempFile().getAbsolutePath(), list.getOutDirectory());
							// inizializzo le visualizzazioni (se presenti)
							support.fireIndexedPropertyChange(PARTIALOP, j, null, EncodingOps.OPENC);
							// esecuzione
							exec(convertArray(commands));
						}
						else
						{
							// operazioni per i wave (sola copia del rip)
							exec(song.getEncoder().getEncoderProgram().postOperations(song.getTempFile()));
						}
						actual+=increment;
						support.firePropertyChange(PROGRESSION, -1, actual);
						support.fireIndexedPropertyChange(PARTIALOP, j, null, EncodingOps.ENDOP);
					}
					// avvertiamo in giro che un'altra traccia e' andata
					if(queue.size()==0 && op.equals( EncodingOps.OPRIP ))
					{
						support.firePropertyChange(EXECLASTPARTIAL, false, true);
					}
					else
					{
						support.firePropertyChange(EXECPARTIAL, false, true);
					}
					j++;
				}
			}
			catch (Exception e)
			{
				support.fireIndexedPropertyChange(PARTIALOP, j, null, EncodingOps.ABROP);
				// verifica dell'interruzione ma non della caduta del processo
				if(breaked && !fallen)
				{
					DaexLogger.getDaexLogger().info("Interruzione dell'utente");
				}
				else
				{
					// lanciare questa eccezione solo da un thread
					// (il primo che se ne accorge)
					if(!fallen)
					{
						fallen=true;
						DaexLogger.getDaexLogger().log(Level.SEVERE, "Errore di esecuzione", e);
					}
				}
			}
			return null;
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#addPropertyChangeListener(String, java.beans.PropertyChangeListener)
		 */
		public void addPropertyChangeListener(String property,PropertyChangeListener listener)
		{
			support.addPropertyChangeListener(property, listener);
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#encode()
		 */
		public void encode()
		{
			// setup encoder del padre
			for(PropertyChangeListener l: DefaultEncoderExecutor.this.support.getPropertyChangeListeners(PROGRESSION))
			{
				this.support.addPropertyChangeListener(PROGRESSION, l);
			}
			for(PropertyChangeListener l: DefaultEncoderExecutor.this.support.getPropertyChangeListeners(EXECOUTPUT))
			{
				this.support.addPropertyChangeListener(EXECOUTPUT, l);
			}
			for(PropertyChangeListener l: DefaultEncoderExecutor.this.support.getPropertyChangeListeners(PARTIALOP))
			{
				this.support.addPropertyChangeListener(PARTIALOP, l);
			}
			// via al thread (dopo apposita segnalazione)
			encoding=true;
			support.firePropertyChange(EXECPROPERTY, !encoding, encoding);
			this.start();
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#getPropertyChangeListeners(String)
		 */
		public PropertyChangeListener[] getPropertyChangeListeners(String property)
		{
			return support.getPropertyChangeListeners(property);
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#isEncoding()
		 */
		public boolean isEncoding()
		{
			return encoding;
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#removePropertyChangeListener(String, java.beans.PropertyChangeListener)
		 */
		public void removePropertyChangeListener(String property, PropertyChangeListener listener)
		{
			support.removePropertyChangeListener(property, listener);
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.SwingWorker#interrupt()
		 */
		@Override
		public void interrupt()
		{
			super.interrupt();
			breaked=true;
			if(p!=null)
			{
				p.destroy();
			}
		}

		private String convertArrayToString(String[] array)
		{
			String result="";
			
			for(String s: array)
				result+=s+" ";
			return result;
		}

		private String[] convertArray(ArrayList<String> array)
		{
			String[] result=new String[array.size()];
			result=array.toArray(result);
			return result;
		}
		
		private void exec(String[] commands) throws Exception
		{
			DaexLogger.getDaexLogger().info("Lancio l'esecuzione: "+convertArrayToString(commands));
			p=new ProcessBuilder(commands).redirectErrorStream(true).start();
			// output della console...
			DaexLogger.getDaexLogger().info("Output del programma:\n");
			InputStream is=p.getInputStream();
			int available=0;
			StringBuffer buf=new StringBuffer();
			while(available!=-1)
			{
				available = is.read();
				if (available != -1)
				{
					char c=(char) available;
					buf.append(c);
					if(c=='\n')
					{
						support.firePropertyChange(EXECOUTPUT, null, buf.toString());
						DaexLogger.getDaexLogger().info(buf.toString());
						buf.delete(0, buf.length());
					}
				}
			}
			int i=p.waitFor();
			if(i!=0)
			{
				throw new ExecutionException("Valore ritornato: "+i);
			}
		}

		/**
		 * Metodo di aggancio di una lista di tracce alla coda,
		 * nell'ordine in cui sono fornite.
		 * 
		 * @param songs le tracce da agganciare
		 */
		public void addSongList(Song[] songs)
		{
			for(Song s: songs)
				queue.offer(s);
		}

		/**
		 * Metodo di aggancio di una nuova traccia alla coda.
		 * 
		 * @param song la nuova traccia
		 */
		public void addSong(Song song)
		{
			queue.offer(song);
		}

		/**
		 * Questo metodo ritorna il valore attuale di baseProgram.
		 * 
		 * @return il valore attuale di baseProgram
		 */
		public Program getBaseProgram()
		{
			return baseProgram;
		}

		/**
		 * Questo metodo setta baseProgram con il valore dato.
		 * 
		 * @param baseProgram il nuovo valore per questo campo
		 */
		public void setBaseProgram(Program baseProgram)
		{
			this.baseProgram = baseProgram;
		}

		/**
		 * Questo metodo ritorna il valore attuale di last.
		 * 
		 * @return il valore attuale di last
		 */
		public boolean isLast()
		{
			return last;
		}

		/**
		 * Questo metodo setta last con il valore dato.
		 * 
		 * @param last il nuovo valore per questo campo
		 */
		public void setLast(boolean last)
		{
			this.last = last;
		}

		/**
		 * Questo metodo ritorna il valore attuale di song.
		 * 
		 * @return il valore attuale di song
		 */
		public Song getSong()
		{
			return song;
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.SwingWorker#finished()
		 */
		@Override
		public void finished()
		{
			encoding=false;
			support.firePropertyChange(EXECPROPERTY, !encoding, encoding);
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#addPropertyChangeListener(java.beans.PropertyChangeListener)
		 */
		public void addPropertyChangeListener(PropertyChangeListener listener)
		{
			support.addPropertyChangeListener(listener);
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#getPropertyChangeListeners()
		 */
		public PropertyChangeListener[] getPropertyChangeListeners()
		{
			return support.getPropertyChangeListeners();
		}

		/**
		 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#removePropertyChangeListener(java.beans.PropertyChangeListener)
		 */
		public void removePropertyChangeListener(PropertyChangeListener listener)
		{
			support.removePropertyChangeListener(listener);
		}
		
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#addPropertyChangeListener(java.beans.PropertyChangeListener)
	 */
	public void addPropertyChangeListener(PropertyChangeListener listener)
	{
		support.addPropertyChangeListener(listener);
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#getPropertyChangeListeners()
	 */
	public PropertyChangeListener[] getPropertyChangeListeners()
	{
		return support.getPropertyChangeListeners();
	}

	/**
	 * @see eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor#removePropertyChangeListener(java.beans.PropertyChangeListener)
	 */
	public void removePropertyChangeListener(PropertyChangeListener listener)
	{
		support.removePropertyChangeListener(listener);
	}

}
